[feat] Surface missing models in Errors tab (Cloud)#9743
Conversation
Detect missing models during workflow load and display them in the right-side-panel Errors tab alongside existing node/prompt errors. - Scan all COMBO widgets (root + subgraphs) for model-like filenames. - Enrich candidates with embedded workflow metadata (url, hash, directory). - Verify asset-supported candidates against the asset store asynchronously. - Surface confirmed missing models via executionErrorStore.surfaceMissingModels. - Add missingModelsError state and related computed properties. - Add hasMissingModelOnNode / isWidgetMissingModel lookup helpers. - Add �ctiveMissingModelGraphIds for canvas-level error indicators. - Add isContainerWithMissingModel for subgraph container badges. - Group missing models by directory with collapsible cards. - MissingModelCard.vue / MissingModelRow.vue: per-model row with library selector (asset-supported) or URL import (Civitai/HuggingFace). - Debounced URL metadata fetch with type-mismatch warnings. - Copy model name button and clear URL input button. - useMissingModelInteractions.ts: composable for all interaction logic. - Highlight nodes and widgets that reference missing models. - Propagate missing-model badges through subgraph containers. - workflowSchema.ts: add lattenWorkflowNodes utility, remove unused getSerializedNodeLabel. - workflowService.ts: simplify surfacePendingWarnings, remove stale widget-detected model merging logic. - odeTitleUtil.ts: remove esolveNodeDisplayLabel wrapper, inline at call site. - ypes.ts: add MissingModelCandidate and EmbeddedModelWithSource. - i18n: add ightSidePanel.missingModels.* keys.
Add colocated tests for new missing-model functions introduced in the previous commit. Also update workflowService.test.ts to reflect the simplified surfacePendingWarnings logic (removed widget-detected model merging and error store assertions that no longer apply). - executionErrorStore.test.ts: surfaceMissingModels, removeMissingModelByName, removeMissingModelsByNodeIds, hasMissingModelOnNode, isWidgetMissingModel - missingModelScan.test.ts: enrichWithEmbeddedMetadata (enrich existing, no-overwrite, add new candidate, skip installed) - useErrorGroups.test.ts: missingModelGroups grouping by directory, unsupported separation, same-name merging, allErrorGroups inclusion - workflowSchema.test.ts: flattenWorkflowNodes (root-only, undefined, subgraph prefixing, nested paths) - MissingModelCard.test.ts: new component test — rendering, props, directory/unsupported groups, event forwarding - workflowService.test.ts: remove stale widget-detected model tests, simplify missing model dialog assertions
- Move interaction state to Pinia store with storeToRefs - Replace mutable accumulator with return values in scan functions - Parallelize checkModelInstalled calls with Promise.all - Add toast notification on polling timeout - Use useModelUpload composable for upload dialog - Make nodeId/sourceNodeId optional in types - Remove unused store functions and their tests - Fix semantic color tokens and cn() usage - Add computed properties to reduce redundant calls
- Replace polling with awaitable promise in asset verification (M-10) - Extract runMissingModelPipeline from loadGraphData (L-22) - Extract MissingModelStatusCard component (M-5) - Extract shared getActiveGraphNodeIds utility (M-4) - Add unit tests for verifyAssetSupportedCandidates, useMissingModelInteractions, and OSS regression - Fix accessibility: role='alert', aria-live, aria-hidden on decorative icons - Fix path traversal sanitization in URL filename extraction
- Return new array from enrichWithEmbeddedMetadata instead of mutating input - Guard against stale surfaceMissingModels call after abort - Remove overly permissive bidirectional path matching in isAssetInstalled - Add AssetVerifier DI to verifyAssetSupportedCandidates - Add onScopeDispose for debounce timer cleanup - Extract MissingModelUrlInput and MissingModelLibrarySelect from MissingModelRow - Use Readonly<ComfyNode>[] return type for flattenWorkflowNodes - Surface toast on asset verification failure - Cache nodeIdStr in processedWidgets loop - Remove unused props destructuring in MissingModelCard
🎭 Playwright: ✅ 552 passed, 0 failed · 4 flaky📊 Browser Reports
|
🎨 Storybook: ✅ Built — View Storybook |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds end-to-end missing-model support: detection, embedded-metadata enrichment, async asset verification, a Pinia missingModel store, interaction composables, new UI components, Errors-tab integration, graph-load pipeline wiring, and node/widget validation updates. Changes
Sequence Diagram(s)sequenceDiagram
participant App as App.loadGraphData()
participant Pipeline as runMissingModelPipeline()
participant Scanner as scanAllModelCandidates
participant Enricher as enrichWithEmbeddedMetadata
participant Verifier as verifyAssetSupportedCandidates
participant MissingStore as missingModelStore
participant ErrorStore as executionErrorStore
participant UI as RightSidePanel
App->>MissingStore: clearMissingModels()
App->>Pipeline: start(graphData)
Pipeline->>Scanner: scanAllModelCandidates(graphData)
Scanner-->>Pipeline: candidates
Pipeline->>Enricher: enrichWithEmbeddedMetadata(candidates)
Enricher-->>Pipeline: enrichedCandidates
Pipeline->>Verifier: verifyAssetSupportedCandidates(enrichedCandidates, signal)
Verifier-->>MissingStore: update isMissing flags
Pipeline->>MissingStore: setMissingModels(enrichedCandidates)
Pipeline->>ErrorStore: surfaceMissingModels(enrichedCandidates)
UI->>MissingStore: read missingModelGroups, activeMissingModelGraphIds
UI->>UI: render MissingModelCard and handle locate-model events
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
📦 Bundle: 4.58 MB gzip 🔴 +11.6 kBDetailsSummary
Category Glance App Entry Points — 17.7 kB (baseline 17.7 kB) • ⚪ 0 BMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 1.01 MB (baseline 974 kB) • 🔴 +36.7 kBGraph editor runtime, canvas, workflow orchestration
Status: 1 added / 1 removed Views & Navigation — 72.5 kB (baseline 72.5 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Status: 11 added / 11 removed Panels & Settings — 438 kB (baseline 438 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
Status: 22 added / 22 removed User & Accounts — 16.1 kB (baseline 16.1 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Status: 7 added / 7 removed Editors & Dialogs — 78.3 kB (baseline 78.3 kB) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
Status: 2 added / 2 removed UI Components — 56.7 kB (baseline 56.7 kB) • ⚪ 0 BReusable component library chunks
Status: 13 added / 13 removed Data & Services — 2.79 MB (baseline 2.78 MB) • 🔴 +15.8 kBStores, services, APIs, and repositories
Status: 16 added / 15 removed Utilities & Hooks — 57.2 kB (baseline 57.2 kB) • ⚪ 0 BHelpers, composables, and utility bundles
Status: 16 added / 16 removed Vendor & Third-Party — 8.9 MB (baseline 8.9 MB) • ⚪ 0 BExternal libraries and shared vendor chunks
Status: 6 added / 6 removed Other — 8.04 MB (baseline 8.04 MB) • 🔴 +1.53 kBBundles that do not match a named category
Status: 120 added / 120 removed |
⚡ Performance Report
Raw data{
"timestamp": "2026-03-12T07:13:32.828Z",
"gitSha": "d7f4a03e8e0f6cd2d7967badbda2f33c74c69cbb",
"branch": "feat/cloud-missing-model-from-errortab",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2016.0730000000058,
"styleRecalcs": 11,
"styleRecalcDurationMs": 9.002999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 358.60900000000004,
"heapDeltaBytes": 1365372
},
{
"name": "canvas-idle",
"durationMs": 2037.238000000002,
"styleRecalcs": 11,
"styleRecalcDurationMs": 10.776,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 376.03700000000003,
"heapDeltaBytes": 762612
},
{
"name": "canvas-idle",
"durationMs": 2033.6840000000507,
"styleRecalcs": 11,
"styleRecalcDurationMs": 10.411999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 364.1979999999999,
"heapDeltaBytes": 1317804
},
{
"name": "canvas-mouse-sweep",
"durationMs": 2006.812000000025,
"styleRecalcs": 83,
"styleRecalcDurationMs": 58.675999999999995,
"layouts": 13,
"layoutDurationMs": 4.5649999999999995,
"taskDurationMs": 884.005,
"heapDeltaBytes": 1557160
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1810.5370000000107,
"styleRecalcs": 74,
"styleRecalcDurationMs": 39.021,
"layouts": 12,
"layoutDurationMs": 3.658,
"taskDurationMs": 756.316,
"heapDeltaBytes": 531660
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1832.9619999999522,
"styleRecalcs": 76,
"styleRecalcDurationMs": 39.245000000000005,
"layouts": 12,
"layoutDurationMs": 3.5370000000000004,
"taskDurationMs": 774.437,
"heapDeltaBytes": 2108932
},
{
"name": "dom-widget-clipping",
"durationMs": 592.0270000000301,
"styleRecalcs": 14,
"styleRecalcDurationMs": 9.824,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 374.215,
"heapDeltaBytes": 12337112
},
{
"name": "dom-widget-clipping",
"durationMs": 600.8539999999698,
"styleRecalcs": 16,
"styleRecalcDurationMs": 13.811,
"layouts": 1,
"layoutDurationMs": 0.19900000000000007,
"taskDurationMs": 357.542,
"heapDeltaBytes": 13332000
},
{
"name": "dom-widget-clipping",
"durationMs": 576.6930000000343,
"styleRecalcs": 14,
"styleRecalcDurationMs": 10.258000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 344.915,
"heapDeltaBytes": 12225000
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 615.7660000000078,
"styleRecalcs": 50,
"styleRecalcDurationMs": 13.304,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 398.429,
"heapDeltaBytes": -4433396
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 601.4790000000403,
"styleRecalcs": 48,
"styleRecalcDurationMs": 12.8,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 399.15299999999996,
"heapDeltaBytes": -4124512
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 562.814000000003,
"styleRecalcs": 48,
"styleRecalcDurationMs": 11.905,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 386.914,
"heapDeltaBytes": -4254356
},
{
"name": "subgraph-idle",
"durationMs": 2004.4510000000173,
"styleRecalcs": 12,
"styleRecalcDurationMs": 11.312,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 338.97900000000004,
"heapDeltaBytes": 831764
},
{
"name": "subgraph-idle",
"durationMs": 2005.8500000000095,
"styleRecalcs": 15,
"styleRecalcDurationMs": 15.612,
"layouts": 1,
"layoutDurationMs": 0.19800000000000004,
"taskDurationMs": 359.873,
"heapDeltaBytes": 1621360
},
{
"name": "subgraph-idle",
"durationMs": 2019.3869999999947,
"styleRecalcs": 12,
"styleRecalcDurationMs": 10.713000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 335.66700000000003,
"heapDeltaBytes": -5954136
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1731.2760000000367,
"styleRecalcs": 76,
"styleRecalcDurationMs": 40.106,
"layouts": 16,
"layoutDurationMs": 4.744,
"taskDurationMs": 714.345,
"heapDeltaBytes": -904472
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1712.615000000028,
"styleRecalcs": 77,
"styleRecalcDurationMs": 37.844,
"layouts": 16,
"layoutDurationMs": 4.335999999999999,
"taskDurationMs": 688.297,
"heapDeltaBytes": -1403884
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1718.0329999999913,
"styleRecalcs": 76,
"styleRecalcDurationMs": 38.671,
"layouts": 16,
"layoutDurationMs": 4.726,
"taskDurationMs": 681.496,
"heapDeltaBytes": -1464652
}
]
} |
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/renderer/extensions/vueNodes/components/LGraphNode.vue (1)
343-358:⚠️ Potential issue | 🟠 MajorSubgraph nodes still miss missing-model error state.
Line 354 passes
nodeLocatorId.value, which is built bysrc/utils/graphTraversalUtil.ts:19-29and includes the subgraph path, intomissingModelStore.hasMissingModelOnNode(). Butsrc/platform/missingModel/missingModelStore.ts:126-136shows that lookup is currently keyed by raw node IDs only. That makes subgraph leaf nodes return false here, so they never get the red outline/footer even when a missing model is present.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/extensions/vueNodes/components/LGraphNode.vue` around lines 343 - 358, The missing-model check is using the full nodeLocatorId (which includes subgraph path) but the missingModelStore keys by raw node IDs, so subgraph leaf nodes miss the error state; change the call(s) that pass nodeLocatorId.value to missingModelStore to use the raw node id (nodeData.id) instead—specifically update missingModelStore.hasMissingModelOnNode(nodeLocatorId.value) to missingModelStore.hasMissingModelOnNode(nodeData.id) (and any other missingModelStore lookups here that currently receive nodeLocatorId.value) so lookups match the store's keying.
🧹 Nitpick comments (3)
src/components/rightSidePanel/errors/useErrorGroups.test.ts (1)
50-55: Drop the unused composable mock from this test too.
useErrorGroups()doesn't load@/platform/missingModel/composables/useMissingModelInteractions, so this mock never participates in the behavior under test. It only expands the fake dependency surface and can drift from production wiring.Based on learnings: "Do not write tests that just test the mocks; ensure tests fail when code behaves unexpectedly."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/rightSidePanel/errors/useErrorGroups.test.ts` around lines 50 - 55, Remove the unused vi.mock call for '@/platform/missingModel/composables/useMissingModelInteractions' from the useErrorGroups.test.ts file because useErrorGroups() does not import or exercise that composable; specifically delete the vi.mock(...) block that returns clearMissingModelState and confirm no other test helpers reference clearMissingModelState, then run the tests to ensure nothing else depended on that fake dependency.src/stores/executionErrorStore.test.ts (1)
21-26: Remove the dead missing-model composable mock.The real
src/platform/missingModel/composables/useMissingModelInteractions.tsmodule doesn't exportclearMissingModelState, anduseExecutionErrorStore()doesn't import that module anyway. This mock can't verify any production behavior and only creates a fake dependency surface that can drift out of sync.Based on learnings: "Do not write tests that just test the mocks; ensure tests fail when code behaves unexpectedly."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/stores/executionErrorStore.test.ts` around lines 21 - 26, Remove the dead mock for '@/platform/missingModel/composables/useMissingModelInteractions' introduced via vi.mock that provides clearMissingModelState; delete that vi.mock block and any references to clearMissingModelState in the test file (executionErrorStore.test.ts) so the test no longer stubs a non-existent export or creates a fake dependency surface, and run the tests to confirm no remaining imports or usages of useMissingModelInteractions remain.src/platform/missingModel/missingModelScan.test.ts (1)
939-948: Strengthen the abort test so it covers the expensive path.This only checks that
isMissingstays unresolved. It still passes ifverifyAssetSupportedCandidates()callsupdateModelsForNodeType()after an already-aborted signal, so it won't catch the stale-work regression in the implementation.💡 Proposed fix
await verifyAssetSupportedCandidates(candidates, controller.signal) // isMissing should remain undefined since we aborted before resolving expect(candidates[0].isMissing).toBeUndefined() + expect(mockUpdateModelsForNodeType).not.toHaveBeenCalled()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platform/missingModel/missingModelScan.test.ts` around lines 939 - 948, Extend the test for verifyAssetSupportedCandidates so it asserts the expensive work path (updateModelsForNodeType) is not invoked when the signal is already aborted: arrange a spy or mock on updateModelsForNodeType (or replace it with a function that would return a slow promise/throw if called), create the aborted AbortController, call verifyAssetSupportedCandidates(candidates, controller.signal), then assert the spy was not called and candidates[0].isMissing is still undefined; this ensures the implementation returns early and does not start the expensive updateModelsForNodeType work.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/rightSidePanel/errors/useErrorGroups.ts`:
- Around line 615-623: The title is using
missingModelStore.missingModelCandidates?.length which counts raw candidates
instead of grouped items; update buildMissingModelGroups() to use the grouped
collection (missingModelGroups.value.length or the grouped array produced by
groupCandidatesByName) for the count in the title so the displayed number
matches the collapsed rows (reference function buildMissingModelGroups and the
reactive missingModelGroups.value).
In `@src/platform/missingModel/components/MissingModelCard.test.ts`:
- Around line 22-38: The test stubbed i18n (const i18n) lacks the
rightSidePanel.missingModels.unknownCategory translation so the test only
matches the raw key; add an entry under messages.en.rightSidePanel.missingModels
like unknownCategory: 'Expected Label' and then update the assertion in
MissingModelCard.test.ts (where it currently checks for the substring
"unknownCategory") to assert the rendered text equals or contains the translated
string instead of the key; search for the literal "unknownCategory" in the test
to find and replace the assertion and ensure the component uses the i18n
instance in the test render.
In `@src/platform/missingModel/components/MissingModelUrlInput.vue`:
- Around line 151-153: The template uses canImportModels but it’s assigned from
flags.privateModelsEnabled which is reactive and can change later; wrap the
access in a computed so the template remains reactive: replace the plain const
canImportModels = flags.privateModelsEnabled with a computed(()=>
flags.privateModelsEnabled) (keep useFeatureFlags and useModelUpload usage
as-is) so the component reads the reactive flag instead of a frozen snapshot.
In `@src/platform/missingModel/composables/useMissingModelInteractions.test.ts`:
- Around line 132-136: The test setup in the beforeEach block uses
vi.clearAllMocks(), which only clears call history and leaks module-scoped mock
implementations like mockDownloadList and mockGetNodeByExecutionId across tests;
replace vi.clearAllMocks() with vi.resetAllMocks() in the beforeEach (the block
that also calls setActivePinia(createPinia()) and sets app.rootGraph) so that
both call history and mock implementations are reset between tests.
In `@src/platform/missingModel/composables/useMissingModelInteractions.ts`:
- Around line 192-210: The store.importCategoryMismatch flag is never cleared
when the user changes the URL or retries an import, leaving
isSelectionConfirmable() blocked; update handleUrlInput to clear
store.importCategoryMismatch[key] (e.g., delete
store.importCategoryMismatch[key] or set it false) when the URL is edited before
setting the debounce timer and also clear the same flag at the start of the
import flow where fetchUrlMetadata or the import-start function is invoked so
every new fetch/import attempt resets importCategoryMismatch.
- Around line 204-245: The fetchUrlMetadata flow can still update
store.urlMetadata, store.urlErrors, or selectedLibraryModel after the input has
changed; add a per-key request token or AbortSignal tied to the debounce key
(the same key used in store.setDebounceTimer) and check it before mutating state
in fetchUrlMetadata (and likewise in uploadAssetAsync handlers around
assetService.getAssetMetadata and uploadAssetAsync results). Generate and store
a unique token/abort controller when scheduling the debounce, pass/attach it to
the async call, and on any new edit/cancel/clear replace/abort the token; before
writing to store.urlMetadata, store.urlErrors, or selectedLibraryModel verify
the token/signal still matches/not aborted and skip updates if stale.
- Around line 177-189: When applying the selected model in the loop that uses
getNodeByExecutionId and referencingNodes, don't just set widget.value; invoke
the widget's normal mutation path by calling widget.callback?.(value) (or the
same call signature used elsewhere) immediately after setting widget.value so
the node's internal logic runs, and ensure the graph is marked dirty/modified
via the same mutation/dirtying flow used elsewhere before calling
store.removeMissingModelByNameOnNodes; in short, for each found widget call
widget.callback?.(value) and trigger the graph-modified/dirty mutation prior to
removing the missing-model entry.
In `@src/platform/missingModel/missingModelScan.ts`:
- Around line 162-187: The enrichment map is keyed only by name
(candidatesByName), causing cross-directory collisions; change the key to
include directory as well (e.g. use `${c.name}::${c.directory ?? ''}` when
inserting into candidatesByName and when looking up from embeddedModels use
`${model.name}::${model.directory ?? ''}`), and optionally keep a fallback
name-only lookup if you need backward compatibility (check
candidatesByName.get(`${model.name}::${model.directory ?? ''}`) first, then
candidatesByName.get(model.name) if not found) so each same-named file in
different directories is matched to the correct embedded model before applying
the `??=` assignments.
- Around line 286-316: The function verifyAssetSupportedCandidates currently
waits until after importing the assets store and after each
updateModelsForNodeType call to check signal.aborted, causing wasted work for
aborted workflows; fix it by checking signal?.aborted immediately after
computing pendingNodeTypes and before importing the assets store (abort early),
and also check signal?.aborted inside the per-nodeType loop before calling
store.updateModelsForNodeType(nodeType) to skip refreshes for already-aborted
signals; keep using the provided assetsStore fallback logic and preserve the
existing try/catch around updateModelsForNodeType.
In `@src/platform/workflow/validation/schemas/workflowSchema.ts`:
- Around line 603-610: graphData.definitions?.subgraphs only collects top-level
subgraph definitions so nested subgraph definitions are omitted; replace direct
usage of graphData.definitions?.subgraphs when building pathMap and
subgraphDefMap with a recursive collector (e.g., collectSubgraphDefinitions)
that walks candidate.definitions?.subgraphs and deduplicates by id using
isSubgraphDefinition; then pass the returned flattened array into
buildSubgraphExecutionPaths(rootNodes, collectedSubgraphs) and use it to
construct subgraphDefMap (map by s.id) so nested subgraphs and their nodes are
included in missing-model detection.
In `@src/stores/executionErrorStore.ts`:
- Around line 98-106: The clearAllErrors() function currently calls
missingModelStore.clearMissingModels(), which wipes in-progress missing-model
resolution state; remove the call to missingModelStore.clearMissingModels() from
clearAllErrors() so it only resets UI/error state (lastExecutionError,
lastPromptError, lastNodeErrors, missingNodesError, isErrorOverlayOpen) and do
not touch missing model importTaskIds/URL/importing flags here; keep the full
missingModelStore.clearMissingModels() only in workflow-load/clean/explicit
reset code paths (where workflow changes are handled) rather than in
clearAllErrors().
---
Outside diff comments:
In `@src/renderer/extensions/vueNodes/components/LGraphNode.vue`:
- Around line 343-358: The missing-model check is using the full nodeLocatorId
(which includes subgraph path) but the missingModelStore keys by raw node IDs,
so subgraph leaf nodes miss the error state; change the call(s) that pass
nodeLocatorId.value to missingModelStore to use the raw node id (nodeData.id)
instead—specifically update
missingModelStore.hasMissingModelOnNode(nodeLocatorId.value) to
missingModelStore.hasMissingModelOnNode(nodeData.id) (and any other
missingModelStore lookups here that currently receive nodeLocatorId.value) so
lookups match the store's keying.
---
Nitpick comments:
In `@src/components/rightSidePanel/errors/useErrorGroups.test.ts`:
- Around line 50-55: Remove the unused vi.mock call for
'@/platform/missingModel/composables/useMissingModelInteractions' from the
useErrorGroups.test.ts file because useErrorGroups() does not import or exercise
that composable; specifically delete the vi.mock(...) block that returns
clearMissingModelState and confirm no other test helpers reference
clearMissingModelState, then run the tests to ensure nothing else depended on
that fake dependency.
In `@src/platform/missingModel/missingModelScan.test.ts`:
- Around line 939-948: Extend the test for verifyAssetSupportedCandidates so it
asserts the expensive work path (updateModelsForNodeType) is not invoked when
the signal is already aborted: arrange a spy or mock on updateModelsForNodeType
(or replace it with a function that would return a slow promise/throw if
called), create the aborted AbortController, call
verifyAssetSupportedCandidates(candidates, controller.signal), then assert the
spy was not called and candidates[0].isMissing is still undefined; this ensures
the implementation returns early and does not start the expensive
updateModelsForNodeType work.
In `@src/stores/executionErrorStore.test.ts`:
- Around line 21-26: Remove the dead mock for
'@/platform/missingModel/composables/useMissingModelInteractions' introduced via
vi.mock that provides clearMissingModelState; delete that vi.mock block and any
references to clearMissingModelState in the test file
(executionErrorStore.test.ts) so the test no longer stubs a non-existent export
or creates a fake dependency surface, and run the tests to confirm no remaining
imports or usages of useMissingModelInteractions remain.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 296f8896-b6eb-4e00-bfec-30bdfdfb492d
📒 Files selected for processing (29)
src/components/rightSidePanel/RightSidePanel.vuesrc/components/rightSidePanel/errors/TabErrors.vuesrc/components/rightSidePanel/errors/types.tssrc/components/rightSidePanel/errors/useErrorGroups.test.tssrc/components/rightSidePanel/errors/useErrorGroups.tssrc/locales/en/main.jsonsrc/platform/missingModel/components/MissingModelCard.test.tssrc/platform/missingModel/components/MissingModelCard.vuesrc/platform/missingModel/components/MissingModelLibrarySelect.vuesrc/platform/missingModel/components/MissingModelRow.vuesrc/platform/missingModel/components/MissingModelStatusCard.vuesrc/platform/missingModel/components/MissingModelUrlInput.vuesrc/platform/missingModel/composables/useMissingModelInteractions.test.tssrc/platform/missingModel/composables/useMissingModelInteractions.tssrc/platform/missingModel/missingModelScan.test.tssrc/platform/missingModel/missingModelScan.tssrc/platform/missingModel/missingModelStore.test.tssrc/platform/missingModel/missingModelStore.tssrc/platform/missingModel/types.tssrc/platform/workflow/core/services/workflowService.tssrc/platform/workflow/validation/schemas/workflowSchema.test.tssrc/platform/workflow/validation/schemas/workflowSchema.tssrc/renderer/extensions/vueNodes/components/LGraphNode.vuesrc/renderer/extensions/vueNodes/components/NodeWidgets.vuesrc/scripts/app.tssrc/stores/assetsStore.tssrc/stores/executionErrorStore.test.tssrc/stores/executionErrorStore.tssrc/utils/graphTraversalUtil.ts
src/platform/missingModel/composables/useMissingModelInteractions.test.ts
Show resolved
Hide resolved
src/platform/missingModel/composables/useMissingModelInteractions.ts
Outdated
Show resolved
Hide resolved
- Use grouped model count instead of raw candidates in error title - Wrap feature flag in computed() for template reactivity - Trigger widget callback and dirty canvas on model substitution - Clear importCategoryMismatch on URL change and import retry - Guard async metadata/import responses with per-key request tokens - Key enrichment map by name+directory to prevent cross-directory collision - Add early abort checks in asset verification loop - Collect nested subgraph definitions recursively for model detection - Preserve missing model state during clearAllErrors() - Fix test mock reset and i18n stub coverage
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
src/platform/missingModel/composables/useMissingModelInteractions.ts (1)
196-215:⚠️ Potential issue | 🟠 MajorInvalidate in-flight requests as soon as the URL changes.
Editing the URL clears the row state, but
_requestTokens[key]is only replaced when the next metadata fetch/import actually starts. If an older request finishes during the debounce window, it can still repopulateurlMetadata,urlErrors, orselectedLibraryModelfor the previous URL. Invalidate the token inhandleUrlInput()before clearing state so older responses are stale immediately.💡 Suggested fix
function handleUrlInput(key: string, value: string) { + _requestTokens[key] = Symbol() store.urlInputs[key] = value delete store.urlMetadata[key] delete store.urlErrors[key]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platform/missingModel/composables/useMissingModelInteractions.ts` around lines 196 - 215, In handleUrlInput, invalidate any in-flight fetch token so old responses can't repopulate state: before clearing row state (store.urlMetadata/urlErrors/selectedLibraryModel) set or replace the per-key request token in the store's _requestTokens map (the same slot fetchUrlMetadata normally sets) to a fresh/neutral value so previous tokens are considered stale; this ensures any older response handlers (which check _requestTokens[key]) will bail out immediately when the URL changes and only the new fetch started by fetchUrlMetadata can update urlMetadata/urlErrors/selectedLibraryModel.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/rightSidePanel/errors/useErrorGroups.ts`:
- Around line 574-623: The missing-model rows aren't being filtered by the
errors search because the computed missingModelGroups (used by TabErrors.vue /
MissingModelCard) always returns the full list; update useErrorGroups to derive
a query-filtered collection (e.g., filteredMissingModelGroups) by applying the
same search logic used for filteredGroups to the groupCandidates/models produced
in missingModelGroups, then have buildMissingModelGroups use that filtered
collection instead of missingModelGroups.value; reference the existing computed
missingModelGroups, the grouping step that calls groupCandidatesByName, and
buildMissingModelGroups so you can reuse or mirror the same query
predicate/filter function to narrow rows based on the current search query.
In `@src/platform/missingModel/missingModelScan.ts`:
- Around line 315-336: When updateModelsForNodeType(nodeType) throws during the
Promise.allSettled stage you must record that nodeType as failed and avoid
classifying any candidates for it; modify the concurrent map over
[...pendingNodeTypes] to add failing nodeTypes into a Set (e.g.,
failedNodeTypes) inside the catch block instead of just logging, then in the
downstream loop over candidates (the for loop using candidates, getAssets, and
isAssetInstalled) skip setting c.isMissing for any candidate whose c.nodeType is
in failedNodeTypes (leave c.isMissing undefined) and preserve the existing
signal?.aborted checks.
---
Duplicate comments:
In `@src/platform/missingModel/composables/useMissingModelInteractions.ts`:
- Around line 196-215: In handleUrlInput, invalidate any in-flight fetch token
so old responses can't repopulate state: before clearing row state
(store.urlMetadata/urlErrors/selectedLibraryModel) set or replace the per-key
request token in the store's _requestTokens map (the same slot fetchUrlMetadata
normally sets) to a fresh/neutral value so previous tokens are considered stale;
this ensures any older response handlers (which check _requestTokens[key]) will
bail out immediately when the URL changes and only the new fetch started by
fetchUrlMetadata can update urlMetadata/urlErrors/selectedLibraryModel.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: fc3df2db-f164-438e-ad13-38a3e5a12d5e
📒 Files selected for processing (8)
src/components/rightSidePanel/errors/useErrorGroups.tssrc/platform/missingModel/components/MissingModelCard.test.tssrc/platform/missingModel/components/MissingModelUrlInput.vuesrc/platform/missingModel/composables/useMissingModelInteractions.test.tssrc/platform/missingModel/composables/useMissingModelInteractions.tssrc/platform/missingModel/missingModelScan.tssrc/platform/workflow/validation/schemas/workflowSchema.tssrc/stores/executionErrorStore.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/platform/workflow/validation/schemas/workflowSchema.ts
- src/platform/missingModel/components/MissingModelUrlInput.vue
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
src/platform/missingModel/missingModelScan.ts (2)
310-313:⚠️ Potential issue | 🟠 MajorAbort again before loading the assets store.
If
signalflips afterpendingNodeTypesis built, this still importsassetsStorefor an abandoned workflow. Add a secondsignal?.abortedguard immediately before the dynamic import.💡 Proposed fix
if (pendingNodeTypes.size === 0) return + if (signal?.aborted) return const store = assetsStore ?? (await import('@/stores/assetsStore')).useAssetsStore()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platform/missingModel/missingModelScan.ts` around lines 310 - 313, Add a second abort check immediately before the dynamic import to avoid loading assetsStore for an abandoned workflow: after the existing pendingNodeTypes.size === 0 return and before the line that sets store via assetsStore ?? (await import(...)).useAssetsStore(), check if signal?.aborted and return early if true. This uses the same signal reference used earlier in missingModelScan.ts and prevents importing '@/stores/assetsStore' when the operation was cancelled.
362-385:⚠️ Potential issue | 🟠 MajorKey the grouped view-model map by directory too.
useErrorGroups()already separates known asset directories, but unsupported and null-directory candidates still arrive here in a shared bucket. Grouping only byc.namecollapses same-named models from different directories into one row and keeps an arbitraryrepresentative, which can then feed the wrongnodeType/metadata intoMissingModelRow.vuefor every reference.💡 Proposed fix
+function groupedCandidateKey(candidate: MissingModelCandidate): string { + return `${candidate.name}::${candidate.directory ?? ''}` +} + export function groupCandidatesByName( candidates: MissingModelCandidate[] ): MissingModelViewModel[] { const map = new Map<string, MissingModelViewModel>() for (const c of candidates) { - const existing = map.get(c.name) + const key = groupedCandidateKey(c) + const existing = map.get(key) if (existing) { if (c.nodeId) { existing.referencingNodes.push({ nodeId: c.nodeId, widgetName: c.widgetName }) } } else { - map.set(c.name, { + map.set(key, { name: c.name, representative: c, referencingNodes: c.nodeId ? [{ nodeId: c.nodeId, widgetName: c.widgetName }] : [] }) } } return Array.from(map.values()) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platform/missingModel/missingModelScan.ts` around lines 362 - 385, groupCandidatesByName currently keys groups only by c.name which merges candidates from different directories; change the grouping key to include the candidate directory (e.g. combine c.directory or a sentinel for null/unsupported with c.name) so each distinct directory+name pair gets its own MissingModelViewModel. Ensure the map key construction and lookup use that combined key, and when creating the group (in the else branch) pick the representative and initial referencingNodes from that candidate as now so metadata/nodeType remains correct per directory group.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/platform/missingModel/missingModelScan.ts`:
- Around line 203-236: The current Promise.allSettled drops models when
checkModelInstalled throws; change the mapping inside unmatched.map in
missingModelScan.ts so that instead of letting checkModelInstalled rejection
bubble, you catch errors locally and return a MissingModelCandidate that
preserves the model info and marks the install check as unresolved (e.g.,
isMissing: undefined) plus an installCheckError field (or similar optional
field) containing the caught error; keep using the existing symbols (unmatched,
checkModelInstalled, isAssetSupported, MissingModelCandidate, enriched) so the
loop can still push non-null values and the failed checks surface to the caller
instead of being skipped.
---
Duplicate comments:
In `@src/platform/missingModel/missingModelScan.ts`:
- Around line 310-313: Add a second abort check immediately before the dynamic
import to avoid loading assetsStore for an abandoned workflow: after the
existing pendingNodeTypes.size === 0 return and before the line that sets store
via assetsStore ?? (await import(...)).useAssetsStore(), check if
signal?.aborted and return early if true. This uses the same signal reference
used earlier in missingModelScan.ts and prevents importing
'@/stores/assetsStore' when the operation was cancelled.
- Around line 362-385: groupCandidatesByName currently keys groups only by
c.name which merges candidates from different directories; change the grouping
key to include the candidate directory (e.g. combine c.directory or a sentinel
for null/unsupported with c.name) so each distinct directory+name pair gets its
own MissingModelViewModel. Ensure the map key construction and lookup use that
combined key, and when creating the group (in the else branch) pick the
representative and initial referencingNodes from that candidate as now so
metadata/nodeType remains correct per directory group.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 4adbf2e0-0f29-4d75-89f3-566724c99f39
📒 Files selected for processing (1)
src/platform/missingModel/missingModelScan.ts
| defineProps<{ | ||
| missingModelGroups: MissingModelGroup[] | ||
| showNodeIdBadge: boolean | ||
| }>() |
There was a problem hiding this comment.
very nit
| defineProps<{ | |
| missingModelGroups: MissingModelGroup[] | |
| showNodeIdBadge: boolean | |
| }>() | |
| const { missingModelGroups, showNodeIdBadge } = defineProps<{ | |
| missingModelGroups: MissingModelGroup[] | |
| showNodeIdBadge: boolean | |
| }>() |
There was a problem hiding this comment.
Done. Applied reactive props destructuring as suggested.
| group.type === 'missing_node' || | ||
| group.type === 'swap_nodes' || | ||
| group.type === 'missing_model' |
There was a problem hiding this comment.
very nit: can we replace it to computed?
There was a problem hiding this comment.
Done. Extracted to a getGroupSize() function using a Set for the full-size group types. Chose a plain function over computed since it takes a parameter (the group) and the Set lookup is O(1).
| aria-hidden="true" | ||
| class="mt-0.5 icon-[lucide--info] size-3.5 shrink-0 text-muted-foreground" | ||
| /> | ||
| <span class="text-[11px] leading-tight text-muted-foreground"> |
There was a problem hiding this comment.
I think we can use tailwind class instead
There was a problem hiding this comment.
Done. Replaced text-[11px] with text-xs.
|
|
||
| <script setup lang="ts"> | ||
| import { useI18n } from 'vue-i18n' | ||
| import SelectPlus from '@/components/primevueOverride/SelectPlus.vue' |
There was a problem hiding this comment.
We decided to stop using PrimeVue and move to Reka UI instead.
There was a problem hiding this comment.
Done. Migrated from PrimeVue SelectPlus to the project's Reka UI Select wrapper. Also added a #prepend slot to SelectContent.vue to support placing a search filter input above the scroll buttons. The search now uses useFuse from @vueuse/integrations for fuzzy matching, which handles large option lists (e.g. 100+ LoRAs) better than simple substring matching.
| confirmLibrarySelect( | ||
| modelKey, | ||
| model.name, | ||
| model.referencingNodes, | ||
| directory | ||
| ) |
There was a problem hiding this comment.
can you replace it with the handler like handleLibrarySelect?
There was a problem hiding this comment.
Done. Extracted the inline confirmLibrarySelect(...) call into a handleLibrarySelect() function.
| v-bind=" | ||
| !canImportModels | ||
| ? { | ||
| role: 'button', | ||
| tabindex: 0, | ||
| onKeydown: (e: KeyboardEvent) => { | ||
| if (e.key === 'Enter' || e.key === ' ') { | ||
| e.preventDefault() | ||
| showUploadDialog() | ||
| } | ||
| } | ||
| } | ||
| : {} | ||
| " |
There was a problem hiding this comment.
This v-bind is getting a bit complex with the conditional object and inline handler. It might be cleaner to move this into a computed prop and reference it from the template.
There was a problem hiding this comment.
Done. Extracted to upgradePromptAttrs computed property.
| </span> | ||
| <span | ||
| v-if="(urlMetadata[modelKey]?.content_length ?? 0) > 0" | ||
| class="shrink-0 rounded-sm bg-secondary-background-selected px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground" |
There was a problem hiding this comment.
I think you can use text-sm or text-xs?
There was a problem hiding this comment.
Done. Replaced text-[10px] with text-xs.
| aria-hidden="true" | ||
| class="mt-0.5 icon-[lucide--triangle-alert] size-3 shrink-0 text-warning-background" | ||
| /> | ||
| <span class="text-[11px] leading-tight text-warning-background"> |
There was a problem hiding this comment.
Done. Replaced text-[11px] with text-xs.
| <i | ||
| aria-hidden="true" | ||
| :class=" | ||
| urlImporting[modelKey] | ||
| ? 'icon-[lucide--loader-circle] size-4 animate-spin' | ||
| : 'icon-[lucide--download] size-4' | ||
| " | ||
| /> |
There was a problem hiding this comment.
Question: Doesn’t the button already have a loading state prop?
There was a problem hiding this comment.
Yes, it does. Replaced the manual spinner icon toggle with :loading="urlImporting[modelKey]" on the Button component.
- Migrate MissingModelLibrarySelect from PrimeVue to Reka UI Select - Add #prepend slot to SelectContent for search filter placement - Use useFuse for fuzzy matching in library select dropdown - Extract inline handlers and complex v-binds to named functions/computeds - Replace arbitrary text sizes with Tailwind classes - Use Button loading prop instead of manual spinner
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/platform/missingModel/components/MissingModelLibrarySelect.vue (1)
76-84: UsedefineModelfor the selected value API.This component already has a
modelValue-shaped contract, but writes go through a separateselectemit. Switching the selected value todefineModel<string | undefined>()keeps the interface idiomatic and lets the extraselectevent remain purely optional for side effects.♻️ Suggested refactor
-const { options, showDivider = false } = defineProps<{ - modelValue: string | undefined +const modelValue = defineModel<string | undefined>() +const { options, showDivider = false } = defineProps<{ options: { name: string; value: string }[] showDivider?: boolean }>() @@ function handleSelect(value: unknown) { if (typeof value === 'string') { filterQuery.value = '' + modelValue.value = value emit('select', value) } }Based on learnings: Applies to
src/**/*.vue: PreferdefineModelto separately defining a prop and emit for v-model bindings.Also applies to: 101-105
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platform/missingModel/components/MissingModelLibrarySelect.vue` around lines 76 - 84, The component defines a v-model style prop/emit pair (modelValue via defineProps and select via defineEmits) instead of using the composition API helper; replace the separate prop and emit with a single defineModel<string | undefined>() call to expose the selected value idiomatically, keep the existing optional select emit (if side-effects needed) but make v-model reads/writes use the defineModel binding, and update any internal usages of modelValue and emits to read/update via the defineModel return value (locate usages around defineProps/defineEmits and in the component template/script where modelValue or emit('select', ...) are referenced).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/platform/missingModel/components/MissingModelLibrarySelect.vue`:
- Around line 25-45: When the search input (template using v-if="options.length
> 4") is hidden the reactive filterQuery remains and can empty the dropdown; add
logic to reset filterQuery when options shrink to 4 or fewer. Implement a
watcher or computed effect on the options array (or options.length) in this
component (referencing filterQuery, options, and the computed filteredOptions)
that sets filterQuery = '' whenever options.length <= 4; apply the same
watcher/fix for the other search block referenced around lines 88-105 so both
input UIs clear their filter when hidden.
---
Nitpick comments:
In `@src/platform/missingModel/components/MissingModelLibrarySelect.vue`:
- Around line 76-84: The component defines a v-model style prop/emit pair
(modelValue via defineProps and select via defineEmits) instead of using the
composition API helper; replace the separate prop and emit with a single
defineModel<string | undefined>() call to expose the selected value
idiomatically, keep the existing optional select emit (if side-effects needed)
but make v-model reads/writes use the defineModel binding, and update any
internal usages of modelValue and emits to read/update via the defineModel
return value (locate usages around defineProps/defineEmits and in the component
template/script where modelValue or emit('select', ...) are referenced).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 2b71dcd6-2cf8-4640-a1f5-82288c1fd175
📒 Files selected for processing (6)
src/components/rightSidePanel/errors/TabErrors.vuesrc/components/ui/select/SelectContent.vuesrc/platform/missingModel/components/MissingModelCard.vuesrc/platform/missingModel/components/MissingModelLibrarySelect.vuesrc/platform/missingModel/components/MissingModelRow.vuesrc/platform/missingModel/components/MissingModelUrlInput.vue
🚧 Files skipped from review as they are similar to previous changes (3)
- src/platform/missingModel/components/MissingModelCard.vue
- src/platform/missingModel/components/MissingModelUrlInput.vue
- src/platform/missingModel/components/MissingModelRow.vue
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/platform/missingModel/components/MissingModelLibrarySelect.vue (1)
76-84: Align this public API with Vuev-modelconventions.
modelValuereads like a standardv-modelprop, but the component only emitsselect. Either expose this asdefineModel()/update:modelValue, or rename the prop to something non-v-model-semantic so the API is not misleading.As per coding guidelines, "Prefer
defineModelto separately defining a prop and emit for v-model bindings."Also applies to: 108-112
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platform/missingModel/components/MissingModelLibrarySelect.vue` around lines 76 - 84, The prop/emit pairing is misleading because the component defines a v-model-looking prop modelValue via defineProps but only emits select; change the public API to follow Vue v-model conventions: either replace the separate defineProps/defineEmits for modelValue and select with defineModel(...) / defineEmits('update:modelValue') (and emit 'update:modelValue' where select is currently emitted), or rename the prop from modelValue to a non-v-model name (e.g., selectedValue) and keep the select emit; update all references to modelValue, select, defineProps, defineEmits, and any emit calls so they match the chosen approach.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/platform/missingModel/components/MissingModelLibrarySelect.vue`:
- Around line 14-21: The SelectTrigger in MissingModelLibrarySelect.vue
currently sets a static :aria-label which overrides the accessible name; remove
the :aria-label="t('rightSidePanel.missingModels.useFromLibrary')" prop from the
SelectTrigger so the accessible name comes from the SelectValue
(placeholder/selected text) instead; locate the SelectTrigger component and
delete that aria-label binding, leaving SelectValue to provide the visible and
accessible name.
---
Nitpick comments:
In `@src/platform/missingModel/components/MissingModelLibrarySelect.vue`:
- Around line 76-84: The prop/emit pairing is misleading because the component
defines a v-model-looking prop modelValue via defineProps but only emits select;
change the public API to follow Vue v-model conventions: either replace the
separate defineProps/defineEmits for modelValue and select with defineModel(...)
/ defineEmits('update:modelValue') (and emit 'update:modelValue' where select is
currently emitted), or rename the prop from modelValue to a non-v-model name
(e.g., selectedValue) and keep the select emit; update all references to
modelValue, select, defineProps, defineEmits, and any emit calls so they match
the chosen approach.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 93934c94-ca2a-42c6-aa37-18ce09870631
📒 Files selected for processing (1)
src/platform/missingModel/components/MissingModelLibrarySelect.vue
Summary
When a workflow is loaded with missing models, users currently have no way to identify or resolve them from within the UI. This PR adds a full missing-model detection and resolution pipeline that surfaces missing models in the Errors tab, allowing users to install or import them without leaving the editor.
Changes
Missing Model Detection
executionErrorStorealongside existing node/prompt errorsErrors Tab UI — Model Resolution
checkpoints,loras,vae) with collapsible category cardsCanvas Integration
Code Cleanup
surfacePendingWarningsin workflowService, remove stale widget-detected model merging logicflattenWorkflowNodesutility to workflowSchema for traversing nested subgraph structuresMissingModelUrlInput,MissingModelLibrarySelect,MissingModelStatusCardas focused single-responsibility componentsTesting
missingModelScan.test.ts): enrichment, skip-installed, subgraph flatteningmissingModelStore.test.ts): state management, removal helpersuseMissingModelInteractions.test.ts): combo select, URL input, import flow, library confirmMissingModelCardand error grouping (useErrorGroups.test.ts)workflowService.test.tsandworkflowSchema.test.tsfor new logicReview Focus
missingModelScan.tsuseMissingModelInteractions.ts— URL metadata fetch, library install, upload fallbackScreenshots
2026-03-12.000826.mp4
┆Issue is synchronized with this Notion page by Unito